/*
* The Mock Socket Class and helper functions. 
* 
* The mock class was designed to resemble the Chrome API, which defines 
* the SocketProperties and SocketInfo.
* 
* SocketProperties is an object with three properties, all of which are 
* optional: persistence(default false), name(user definied string associated 
* with socketId), bufferSize(size of buffer used to receive data whose default 
* value is 4096).
*
* SocketInfo contains all the information a user might need regarding a socket
* including socketId, localAddress, localPort, peerAddress, peerPort, etc.
* These properties are set within the Socket class.
*/
'use strict'
var socketsCreated = {};
var socketsTotal = 0;
var nextlocalPort = 1049;
var nextServerPort = 6700;
var dataSocket = null;
var dataConnections = {};
var lastCommand = null;
var MockSocket = {};
MockSocket.onReceive = {};
MockSocket.onReceive.callbacks = [];
MockSocket.onReceiveError = {};
MockSocket.onReceiveError.callbacks = [];

// A few error codes as defined for the Chrome socket API
var errors = {
  FAILED: -2,
  UNEXPECTED: -9,
  SOCKET_NOT_CONNECTED: -15,
  CONNECTION_REFUSED: -102
};

var enableDebug = false;

function print (message) {
  if (enableDebug) {
    console.log(message);
  }
}

function constructServerResponse(reply) {
  var encoder = new TextEncoder("utf-8");
  var response = encoder.encode(reply);
  return response;
}

function connect (socketObj, peerAddress, peerPort, callback) {
  var result = 0;
  var socketId = socketObj.socketId;
  if (typeof(socketId) != "number" || (socketId < 0) ||
    typeof(peerPort) != "number" || isNaN(peerPort) || (peerPort < 0) ||
  typeof(peerAddress) != "string" || peerAddress === "") {
    result = errors.UNEXPECTED;
  } else {
    socketObj.peerAddress = peerAddress;
    socketObj.peerPort = peerPort;
    socketObj.connected = true;
    result = 0;
  }
  if (typeof(callback) == "function"){
    callback(result);
  }
  if (result === 0) {
    //If connect succeeded, then send the 220 server message to the user
    var responseArray = constructServerResponse("220 Service ready for new user\r\n");
    var connectInfo = {
      socketId: socketId,
      data: responseArray
    };
    for (var i = 0; i < MockSocket.onReceive.callbacks.length; i++) {
      MockSocket.onReceive.callbacks[i](connectInfo);
    }
  }
}

MockSocket.connect = function (socketId, peerAddress, peerPort, callback) {
  var socketObj = socketsCreated[socketId];
  if (socketObj === undefined) {
    callback(errors.FAILED); // a generic failure occured
  } else {
    for (var commandId in dataConnections) {
      if (peerPort == dataConnections[commandId].dataPort) {
        // requested a data connection. Remembering the socketId of the data
        // connection is neccessary when we send data through the data connection
        dataConnections[commandId].dataId = socketId;
      }
    }
    connect(socketObj, peerAddress, peerPort, callback);
  }
};

MockSocket.onReceive.addListener = function (callback) {
  //remembering the callback function to notify user when data is "received"
  //from the server.
  MockSocket.onReceive.callbacks.push(callback);
};

MockSocket.onReceiveError.addListener = function (callback) {
  MockSocket.onReceiveError.callbacks.push(callback);
};

MockSocket.onReceive.removeListener = function (callback) {
  var i = MockSocket.onReceive.callbacks.indexOf(callback);
  if (i != -1) {
    MockSocket.onReceive.callbacks.splice(i, 1);
  }
};

MockSocket.onReceiveError.removeListener = function (callback) {
  var i = MockSocket.onReceiveError.callbacks.indexOf(callback);
  if (i != -1) {
    MockSocket.onReceiveError.callbacks.splice(i, 1);
  }
};

MockSocket.create = function (properties, callback) {
  var name;
  if (typeof(properties.name) != "undefined") {
    name = properties.name;
  }
  socketsTotal++;
  var socketObj = {
    connected: false,
    localAddress: "1324:0:1032:1:cd15:c41e:df8e:4f79",
    localPort: nextlocalPort,
    paused: false,
    persistent: properties.persistent || false,
    socketId: socketsTotal,
    peerAddress: "",
    peerPort: 0,
    bufferSize: properties.bufferSize || 4096,
    name: name || ""
  };
  nextlocalPort++;
  socketsCreated[socketObj.socketId] = socketObj;
  var createInfo = {"socketId": socketObj.socketId};
  if (typeof(callback) == "function") {
    callback(createInfo);
  }
};

MockSocket.getInfo = function (socketId, callback) {
  var socketObj = socketsCreated[socketId];
  callback(socketObj);
};

function isCommandCode(commandCode) {
  var legalCodes = [
    "USER", "PASS", "LIST", "ACCT", "NLIST", "CWD", "QUIT", "PASV", "PORT",
    "TYPE", "STRU", "MODE", "EPSV", "NOOP", "CDUP", "PWD"
  ];
  if (commandCode.length > 4 || legalCodes.indexOf(commandCode) < 0) {
    // command codes are 4 or fewer characters
    return false;
  }
  return true;
}

function getServerResponse(command, socketId) {
  var commandCode = /\w*/.exec(command);
  var response;
  if (commandCode === null || !isCommandCode(commandCode[0])) {
    // command codes are 4 or fewer characters
    response = "500 Unknown command\r\n";
  } else {
    // valid FTP command issued, issue an appropriate response
    switch (commandCode[0]) {
      case "USER":
        var username = /\s\S*\r\n/.exec(command);
        if (username === null) {
          response = "501 Syntax error in parameters or arguments\r\n";
        } else {
          username = /\s\S*/.exec(username[0]);
          if (username === null) {
            response = "501 Syntax erro in parameters or arguments\r\n";
          } else {
          response = "331 User name okay, need password\r\n";
          }
        }
        break;
      case "PASS":
        var password = /\s\S*\r\n/.exec(command);
        if (password === null) {
          response = "501 Syntax error in parameters or arguments\r\n";
        } else {
          password = /\s\S*/.exec(password[0]);
          if (password === null) {
            response = "501 Syntax erro in parameters or arguments\r\n";
          } else if (lastCommand != "USER") {
            response = "530 Not logged in\r\n";
          } else {
            response = "230 User logged in, proceed\r\n";
          }
        }
        break;
      case "PASV":
        var serverPort = nextServerPort;
        dataConnections[socketId] = {"dataPort": serverPort, dataId: null};
        nextServerPort++;
        var portHigherBits = (serverPort & 0xFF00)>>8;
        var portLowerBits = (serverPort & 0xFF);
        response = ("227 Entering passive mode " + "(0,0,0,0," +
        portHigherBits + "," + portLowerBits +")\r\n");
        break;
      case "EPSV":
        var serverPort = nextServerPort;
        dataConnections[socketId] = {"dataPort": serverPort, dataId: null};
        nextServerPort++;
        response = ("227 Entering passive mode " + "(|||" + serverPort +
            "|)\r\n");
        break;
      case "NOOP":
        if (command != "NOOP\r\n") {
          response = "501 Syntax erro in parameters or arguments\r\n";
        } else {
          response = "220 OK\r\n";
        }
        break;
      case "PWD":
        response = "257 \"/home/ftp\"\r\n";
        break;
      case "CDUP":
        response = "200 Directory changed\r\n";
        break;
      case "CWD":
        response = "250 Directory changed\r\n";
        break;
      default:
        response = "500 Unknown command\r\n";
      }
  }
  response = constructServerResponse(response);
  lastCommand = commandCode[0]; // some commands are immediately preceded
      // by other commands, hence remember last command
    return response;
}


MockSocket.send = function (socketId, data, callback) {
  var socketObj = socketsCreated[socketId], decoder = new TextDecoder("utf-8");
  if (socketObj === undefined) {
    callback({resultCode: errors.FAILED, bytesSent: 0});
  } else if (!socketObj.connected) {
    callback({resultCode: errors.SOCKET_NOT_CONNECTED, bytesSent: 0});
  } else if (socketObj.paused) {
    callback({resultCode: errors.FAILED, bytesSent: 0});
  } else {
    var dataStr = decoder.decode(new Uint8Array(data));
    var responseArray = getServerResponse(dataStr, socketId);
    var sendInfo = {
      socketId: socketId,
      data: responseArray
    };
    // acknowledge receit of data
    callback({resultCode: 0, bytesSent: data.byteLength});
    // send server response
    for (var i = 0; i < MockSocket.onReceive.callbacks.length; i++) {
      MockSocket.onReceive.callbacks[i](sendInfo);
    }
  }
};
